CQRS Pattern
3 min readCQRS Pattern
TL;DR
CQRS (Command Query Responsibility Segregation) splits the write path (commands that mutate state and enforce invariants) from the read path (queries that fetch denormalized projections), so each side scales, optimizes, and secures independently. It pairs naturally with MediatR pipelines and event sourcing, but adds complexity — reach for it only when read/write workloads diverge or you need an audit trail.
How it works
🧩 Example — PlaceOrder (Command) vs GetOrder (Query)
public record PlaceOrderCommand(string Symbol, double Amount);
public record GetOrderQuery(Guid OrderId);
public class Order
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Symbol { get; set; }
public double Amount { get; set; }
}
public class OrderCommandHandler
{
private readonly ITradeExecutor _executor;
public OrderCommandHandler(ITradeExecutor executor) => _executor = executor;
public void Handle(PlaceOrderCommand command)
{
var order = new Order { Symbol = command.Symbol, Amount = command.Amount };
_executor.Execute(order);
}
}
public class OrderQueryHandler
{
private readonly Dictionary<Guid, Order> _orders = new();
public Order Handle(GetOrderQuery query)
=> _orders.TryGetValue(query.OrderId, out var order)
? order
: throw new KeyNotFoundException("Order not found");
}
// --- Usage ---
var executor = new Mt5Executor();
var commandHandler = new OrderCommandHandler(executor);
commandHandler.Handle(new PlaceOrderCommand("EURUSD", 1000));
var queryHandler = new OrderQueryHandler();
// queryHandler.Handle(new GetOrderQuery(...));
✅ Why it matters:
- Commands → mutate state (place/cancel order).
- Queries → fetch data (get portfolio, prices).
- Enables scalability (separate read/write services) and event sourcing (audit trading actions).
Quick recall Q&A
It allows optimizing writes (placing trades, cancelling orders) separately from reads (dashboards, risk reports). Each side scales independently, and commands can enforce invariants while queries use denormalized projections for speed.
MediatR naturally models CQRS—commands and queries implement IRequest<T> handled by dedicated handlers. Pipeline behaviors add validation, logging, or retries without mixing concerns.
Prefer returning void or minimal identifiers, encouraging clients to query for the latest state separately. This keeps commands focused on side effects and simplifies testing.
Use domain events or the outbox pattern to publish changes that projection handlers consume. They update materialized views, caches, or search indexes asynchronously.
Commands append events to a store. Queries rebuild state or read from projections fed by those events. This provides a complete audit trail for compliance-heavy domains.
For small services with simple CRUD needs, splitting handlers adds complexity. Start simple and adopt CQRS when read/write workloads diverge or when you need auditability and segregation.
Apply behaviors or decorators on the command pipeline to run validators and authorization checks pre-handler. Queries can apply read-specific policies separately.
Commands can target a write-optimized store (e.g., SQL with strict constraints) while queries hit read replicas or NoSQL caches tuned for fast lookups. This reduces contention and lock waits.
Unit test commands with mocked dependencies to assert events or repository calls. Integration test queries against seed data or projections to ensure mapping and filtering work as expected.
Accept eventual consistency for views, communicate lag expectations, and provide read-your-writes mechanisms when necessary (e.g., direct query fallback or wait-for-projection acknowledgments).